package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.reponsitory.GoodsReponsitory;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 用来生成elasticsearch数据的service方法。
 */
@Service
public class SearchService {

    @Autowired
    private SpecificationClient specificationClient;
    @Autowired
    private CategoryClient categoryClient;
    @Autowired
    private GoodsClient goodsClient;
    @Autowired
    private BrandClient brandClient;

    @Autowired
    private GoodsReponsitory goodsReponsitory;  // elasticsearch查询接口

    // ObjectMapper类是Jackson库的主要类。它提供一些功能将转换成Java对象匹配JSON结构，反之亦然
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 封装创建索引库数据的方法，把这些数据都放到goods对象里返回。
     *
     * @param spu
     * @return
     * @throws IOException
     */
    public Goods bulidGoods(Spu spu) throws IOException {
        Goods goods = new Goods();

        // 1、设置id就设置成spu的id
        goods.setId(spu.getId());
        // 2、设置类目的id
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        // 3、获取并设置品牌id
        goods.setBrandId(spu.getBrandId());
        // 4、设置创建时间
        goods.setCreateTime(spu.getCreateTime());
        // 5、设置卖点 subtitle
        goods.setSubTitle(spu.getSubTitle());

        // 6、设置all的分类标题品牌数据，先搞定品牌名称,再去用接口定义的方法spuid查询品牌，得到spu的title+品牌名称+分类的名称
        Brand brand = this.brandClient.BrandByCidForEs(spu.getBrandId());
        List<String> cids = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        goods.setAll(spu.getTitle() + brand.getName() + StringUtils.join(cids, " "));

        // 7、************设置根据spuid查到的所有sku
        List<Sku> skus = this.goodsClient.querySku(spu.getId());
        List<Long> prices = new ArrayList<>();  // sku价格的集合

        List<Map<String, Object>> MapLists = new ArrayList<>(); // sku其他属性的map集合
        skus.forEach(sku -> {           // 遍历获取的sku集合
            prices.add(sku.getPrice()); // 获取价格集合
            Map<String, Object> skuMap = new HashMap<>();  // 把sku里筛选需要的属性封装到一个map集合里
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("price", sku.getPrice());
            skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ?
                    StringUtils.split(sku.getImages(), ",")[0] : "");

            MapLists.add(skuMap);  // 把封装的数据放到属性的map集合里。
        });
        goods.setPrice(prices);  // 设置
        goods.setSkus(MAPPER.writeValueAsString(MapLists));  // 使用java对象转json工具转成json，这样的话用es能更快查询，

        // 8、******* 获取规格参数数据specs，先根据cid3获取设置所有的规格参数以及值，
        List<SpecParam> params = this.specificationClient.queryParamsByCid(null, spu.getCid3(), null, null); // 根据cid获取所有的param参数
        // 再查询spu对应的规格参数，也就是spuDetail的数据
        SpuDetail spuDetail = this.goodsClient.querySpuDetail(spu.getId());

        // 把获取到的spuDetail里的标准规格参数使用ObjectMapper转换成map类型,也就是Generic_Spec字段，有异常直接抛出（每个spu对应单独的spuDetail）
        // 把这些数据"1":"魅族（MEIZU）","2":"魅蓝X","3":2016.0,"5":165... 转换成map的key value的格式，反序列化
        Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() {
        });
        // 把获取到的spuDetail里非标的规格参数，Special_Spec字段放到map里
        // 转换这些数据{"4":["香槟金（白）","玫瑰金"],"12":["4GB"],"13":["64GB"]}
        Map<Long, Object> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, Object>>() {
        });
        // 封装数据,定义map接收，所有参数名称：值（品牌：xx,型号：xx,上市日期:xx）这些。
        Map<String, Object> paramMap = new HashMap<>();

        //循环规格参数，
        params.forEach(param -> {
            // 先判断是否是通用规格参数
            if (param.getGeneric()) {
                String value = genericSpecMap.get(param.getId()).toString(); // 获取通用参数的值
                // 判断是否是数值类型,如果是数值的话，判断该数值落在那个区间，这个使用一个单独的方法来处理。
                if (param.getNumeric()) {
                    // 范围值需要处理下
                    value = chooseSegment(value, param);
                }
                // 把参数名称和参数的属性放到结果集，根据tb_spec_param的id对应tb_spu_detail表里generic_spec字段，封装成属性
                paramMap.put(param.getName(), value);
            } else {
                // 如果不是特殊的规格参数，直接封装到map里
                paramMap.put(param.getName(), specialSpecMap.get(param.getId()));
            }
        });
        goods.setSpecs(paramMap);  // 得到的数据封装到goods的集合中，将来key就是elasticsearch的key,value就是els的value
        //"分辨率" -> "1920*1080(FHD)"
        //"品牌" -> "其它"
        //"CPU核数" -> "八核"
        //"后置摄像头" -> "1000-1500万"
        //"机身重量（g）" -> "g以上"
        //"型号" -> "CAZ-AL10"

        // 至此封装完毕！
        return goods;
    }


    // 处理区间数组的方法
    private String chooseSegment(String value, SpecParam param) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : param.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + param.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + param.getUnit() + "以下";
                } else {
                    result = segment + param.getUnit();
                }
                break;
            }
        }
        return result;
    }


    /**
     * 从索引库查询的方法
     *
     * @param request
     * @return
     */
    public SearchResult search(SearchRequest request) {
        //1、获取关键字，判断参数
        String key = request.getKey();
        if (StringUtils.isBlank(key)) {
            return null;
        }

        // 2、构建查询
        NativeSearchQueryBuilder QueryBuilder = new NativeSearchQueryBuilder();


        // 对key进行全文检索查询,如果多个分词使用and
        //MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);

        // 后面的筛选查询单独封装成一个方法,
        BoolQueryBuilder boolQueryBuilder = buildBooleanQueryBuilder(request);

        QueryBuilder.withQuery(boolQueryBuilder);
        // 使用filter过滤只要的字段，否则返回一堆没用的数据，影响查询效率。
        QueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));
        // 设置分页
        Integer page = request.getPage();
        Integer size = request.getSize();
        QueryBuilder.withPageable(PageRequest.of(page - 1, size));

        // 2.5、排序
        String sortBy = request.getSortBy();
        Boolean descending = request.getDescending();
        if (!StringUtils.isBlank(sortBy)) {
            // 排序后再判断descending的值，如果为true倒叙，默认正序
            QueryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC));
        }

        // 2.6 构建聚合，品牌和分类需要聚合，得到2个结果
        String categoryAggName = "categories";  // 聚合名称
        String brandsAggName = "brands";        // 聚合名称
        QueryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); // 根据cid3聚合
        QueryBuilder.addAggregation(AggregationBuilders.terms(brandsAggName).field("brandId")); // 根据品牌id聚合

        // 3、进行聚合查询(需要强转)，查出来的结果再聚合
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) goodsReponsitory.search(QueryBuilder.build());

        // 定义2个方法getCategoryAgg、getBrandAgg 用来获取分类聚合结果，调下面的方法
        List<Map<String, Object>> categories = getCategoryAgg(goodsPage.getAggregation(categoryAggName));
        List<Brand> brands = getBrandAgg(goodsPage.getAggregation(brandsAggName));

        // 判断分类聚合的结果集大小，等于1则聚合,也就是第一个分类是不是选了，获取到参数
        List<Map<String, Object>> specs = null;
        if (categories.size() == 1) {
            specs = getParamAgg((Long) categories.get(0).get("id"), boolQueryBuilder);
        }

        // 4、封装结果并返回，总条数，总页码，数据、聚合的分类，和品牌数据
        return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(),
                goodsPage.getContent(), categories, brands, specs);
    }

    // 后期多条件筛选，封装的bool查询构建器，之前不是组合查询这个给加上组合
    private BoolQueryBuilder buildBooleanQueryBuilder(SearchRequest request) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 添加基本查询条件,去all里去查
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));

        // 判断是否有过滤条件.如果没有过滤。直接返回，Filter是传过来的过滤参数，比如一些参数CPU 内存什么的
        if (CollectionUtils.isEmpty(request.getFilter())) {
            return boolQueryBuilder;
        }

        // 如果有过滤条件，处理数据。
        for (Map.Entry<String, String> entry : request.getFilter().entrySet()) {
            // 获取k值，品牌，CPU核数这些
            String key = entry.getKey();
            if (StringUtils.equals("品牌", key)) {  // 如果是品牌使用brandId过滤,如果是分类用cid3
                key = "brandId";
            } else if (StringUtils.equals("分类", key)) {
                key = "cid3";
            } else {
                // 如果是规格参数名，或者过滤名的话：specs.xxx.keyword
                key = "specs." + key + ".keyword";
            }
            // 分词查询数据
            boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue()));
        }

        return boolQueryBuilder;

    }


    // 聚合出规格参数的过滤条件
    private List<Map<String, Object>> getParamAgg(Long id, QueryBuilder basicQuery) {
        //创建自定义查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 基于基本的查询条件，聚合规格参数
        queryBuilder.withQuery(basicQuery);
        // 查询要聚合的规格参数
        List<SpecParam> params = this.specificationClient.queryParamsByCid(null, id, null, true);
        // 添加聚合，给每个规格参数添加聚合条件，比如给CPU核数聚合：param.getName()就是 "CPU核数" , 后面的条件就是 "specs.CPU核数.keyword"  代表不分词
        params.forEach(param -> {
            queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName()
                    + ".keyword"));
        });
        // 只需要聚合结果集，不需要查询结果集
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));

        // 执行聚合查询
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsReponsitory.search(queryBuilder.build());

        // 定义一个集合收集聚合结果集
        List<Map<String, Object>> paramMapList = new ArrayList<>();
        // 解析聚合查询的结果集
        Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();
        //aggregationMap.forEach((name, agg) -> {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("k", name);
        //
        //    List<String> options = new ArrayList<>();
        //    StringTerms stringTerms = (StringTerms) agg;
        //    stringTerms.getBuckets().forEach(bucket -> {
        //        options.add(bucket.getKeyAsString());
        //    });
        //
        //    map.put("options", options);
        //    paramMapList.add(map);
        //});


        for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {
            Map<String, Object> map = new HashMap<>();
            // 放入规格参数名
            map.put("k", entry.getKey());
            // 收集规格的参数值
            List<String> options = new ArrayList<>();
            //解析每个聚合
            Aggregation value = entry.getValue();
            StringTerms terms = (StringTerms) value;
            //StringTerms terms = (StringTerms)entry.getValue();
            // 遍历每个聚合中桶，把桶中的key放入收集规格参数的集合中
            terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString()));
            map.put("options", options);
            paramMapList.add(map);
        }
        return paramMapList;
    }


    // 聚合根据cid3来 解析分类id聚合的方法
    private List<Map<String, Object>> getCategoryAgg(Aggregation aggregation) {
        //1、处理聚合结果集
        LongTerms terms = (LongTerms) aggregation;

        // 2、获取所有分类的id桶
        List<LongTerms.Bucket> buckets = terms.getBuckets();

        // 3、处理每一个桶，封装再返回
        // 根据桶获取品牌id
        return buckets.stream().map(bucket -> {
            Map<String, Object> map = new HashMap<>();
            long id = bucket.getKeyAsNumber().longValue();      // 获取聚合的分类ID
            List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(id)); // 根据分类ID得到所有的分类名称
            map.put("id", id);
            map.put("name", names.get(0));
            return map;
        }).collect(Collectors.toList());
    }

    // 聚合根据品牌ID来解析品牌分类聚合的方法
    private List<Brand> getBrandAgg(Aggregation aggregation) {
        // 1、处理聚合数据
        LongTerms terms = (LongTerms) aggregation;

        // 2 得到
        return terms.getBuckets().stream().map(bucket -> {
            List<Brand> brands = new ArrayList<>();
            long id = bucket.getKeyAsNumber().longValue(); // 品牌ID
            return this.brandClient.BrandByCidForEs(id);  // 从每个聚合中查出品牌
        }).collect(Collectors.toList());
    }


    /*
        用来接收create和insert消息后创建或者更新es里goods的方法。。
        如果遇到异常直接往外抛就行。MQ会处理
     */
    public void createIndex(Long id) throws IOException {
        // 根据spuId查询spu
        Spu spu = this.goodsClient.querySpuById(id);
        if (spu == null) {
            return;
        }
        // 构建商品-在es里重新构建其实就是更新。。。
        Goods goods = this.bulidGoods(spu);

        // 保存到索引数据库
        this.goodsReponsitory.save(goods);
    }

    /*
        用来接收delete消息后创建或者更新es里goods的方法。。
        直接根据id删除即可
     */
    public void deleteIndex(Long id) {
        this.goodsReponsitory.deleteById(id);
    }


}
